import { DeviceInputKey, InputStatus } from "babylon-lib/preset/device-input/device-information";
import { DefaultStage } from "../default-stage";
import { DeviceInputPreset } from 'babylon-lib/preset/device-input/device-input-preset';
import { Quaternion, Vector3 } from "@babylonjs/core/Maths/math.vector";
import { Helper } from "babylon-lib/tool/helper";
import { UniversalCamera } from "@babylonjs/core/Cameras/universalCamera";
import { Axis } from '@babylonjs/core/Maths/math.axis';
import { UAV } from "./uav";
import { ITimer, AnimationFrame } from "babylon-lib/animation/animation-frame";
import { PointerInfo } from "@babylonjs/core/Events/pointerEvents";
import { DEGREE } from "babylon-lib/tool/calculation";
import { WebXRControllerComponent } from "@babylonjs/core";
import { State } from "babylon-lib/misc/state";


enum KEY {
    forward,
    backward,
    left,
    right
}


class InputConfig {

    private static _movementReq: ITimer = {};
    private static _inertia = 0;
    private static _attenuation = 0;
    private static _orientateReq: ITimer = {};
    private static _orientateInertia = 0;
    private static _orientateAttenuation = 0;
    private static _tv0 = new Vector3();
    private static _tv1 = new Vector3();
    private static _tv2 = new Vector3();
    private static _tq0 = Quaternion.Identity();
    private static _tq1 = Quaternion.Identity();
    private static _pointerDx = 0;
    private static _pointerDy = 0;
    private static _tv3 = new Vector3();
    private static _tv4 = new Vector3();
    private static _tv5 = new Vector3();
    private static _orinKs = 0;
    private static _keyState = new State();

    public static moveSpeed = 0.4;
    public static orientateSpeed = 0.4;
    public static moveDirection = new Vector3();

    public static initialize(stage: DefaultStage): void {
        DeviceInputPreset.initialize(stage.mainScene, { checkInterval: 100 });
        this.setPointerCameraInput(stage);
        this.setGeneralCameraInput(stage);
        this.setFightInput(stage);
        this.setXRInput(stage)
    }

    private static setXRInput(stage: DefaultStage): void {
        if (stage.vr) {
            let thumb: WebXRControllerComponent | null = null;
            let squeeze: WebXRControllerComponent | null = null;

            let start = false;
            const _this = InputConfig;
            if (stage.vr.xrInput) {
                stage.vr.xrInput.onAdded((inputSource) => {
                    inputSource.onMotionControllerInitObservable.addOnce(() => {
                        thumb = inputSource.motionController!.getComponentOfType('thumbstick');
                        squeeze = inputSource.motionController!.getComponentOfType('squeeze');
                        thumb!.onAxisValueChangedObservable.add((paras) => {
                            const dx = paras.x;
                            const dy = paras.y;
                            if (dx === 0 && dy === 0) {
                                start = false;
                                _this.movementAction(stage, false, 0, 0);
                            } else {
                                _this.movementAction(stage, true, dx, -dy);
                                start = true;
                            }
                        });
                        squeeze!.onButtonStateChangedObservable.add((paras) => {
                            if (paras.changes.pressed && paras.changes.pressed.current) {
                                UAV.dropBomb();
                            }
                        });
                    });
                });
            }

        }
    }

    private static setPointerCameraInput(stage: DefaultStage): void {
        const _this = InputConfig;
        const pointerIds: number[] = [];
        const pointerDrs: boolean[] = [];
        const pointerXs: number[] = [];
        const pointerYs: number[] = [];
        let ratio = 1;
        let isLeft = false;
        let start = false;
        DeviceInputPreset.onInput((e) => {
            const evt = (e as PointerInfo).event as PointerEvent;
            if (pointerIds.includes(evt.pointerId)) return;
            ratio = 1000 / window.innerHeight;
            pointerIds.push(evt.pointerId);
            isLeft = (evt.clientX < window.innerWidth * 0.5) && (this._keyState.notExist(KEY.forward, KEY.backward, KEY.left, KEY.right));
            pointerDrs.push(isLeft);
            pointerXs.push(evt.clientX * ratio);
            pointerYs.push(evt.clientY * ratio);
            start = true;
            isLeft ? _this.movementAction(stage, true, 0, 0) :
                _this.orientateAction(stage, true, 0, 0, true);
        }, InputStatus.down, DeviceInputKey.pointer);

        DeviceInputPreset.onInput((e) => {
            if (!start) return;
            const evt = (e as PointerInfo).event as PointerEvent;
            const x = evt.clientX * ratio;
            const y = evt.clientY * ratio;
            const id = evt.pointerId;
            const idx = pointerIds.indexOf(id);
            if (idx < 0) return;
            const dx = x - pointerXs[idx];
            const dy = y - pointerYs[idx];
            pointerDrs[idx] ? _this.movementAction(stage, true, Math.min(dx * 0.004, 1), -Math.min(dy * 0.004, 1)) :
                _this.orientateAction(stage, true, -Math.min(dx * 0.01, 1), Math.min(dy * 0.01, 1));
        }, InputStatus.move, DeviceInputKey.pointer);

        DeviceInputPreset.onInput((e) => {
            const evt = (e as PointerInfo).event as PointerEvent;
            const id = evt.pointerId;
            const idx = pointerIds.indexOf(id);
            if (idx < 0) return;
            pointerDrs[idx] ? _this.movementAction(stage, false, 0, 0) : _this.orientateAction(stage, false, 0, 0);
            pointerXs.splice(idx, 1);
            pointerYs.splice(idx, 1);
            pointerIds.splice(idx, 1);
            pointerDrs.splice(idx, 1);
            if (pointerIds.length === 0) start = false;
        }, InputStatus.up, DeviceInputKey.pointer);
    }

    private static orientateAction(stage: DefaultStage, play: boolean, dx: number, dy: number, increase?: boolean): void {
        if (!play) {
            this._orinKs--;
            if (this._orinKs <= 0) this._orientateAttenuation = 0.85;
        } else {
            this._pointerDx = dx!;
            this._pointerDy = dy!;
            this._orientateInertia = Math.min(Math.sqrt(dx! * dx! + dy! * dy!), 1);
            this._orientateAttenuation = 1;

            if (increase) this._orinKs++;
        }
        if (AnimationFrame.isRunning(this._orientateReq)) return;
        this._orientateReq.observable = stage.mainScene.onBeforeRenderObservable;
        const _this = InputConfig;
        AnimationFrame.request((t: number) => {
            const cam = stage.mainCamera as UniversalCamera;
            const engine = stage.engine;
            const spd = _this.orientateSpeed * Math.sqrt(t / (engine.getFps() * 100));
            _this._orientateInertia *= _this._orientateAttenuation;
            const tar = this._tv3.copyFrom(cam.target).subtractInPlace(cam.globalPosition);
            const forward = _this.getCamDirection(stage, this._tv4);
            const left = Vector3.CrossToRef(Axis.Y, forward, _this._tv5).normalize();
            const ang = Vector3.GetAngleBetweenVectors(tar, forward, left);

            const rin = -90 * DEGREE + 1e-6 - ang;
            const rax = 90 * DEGREE - 1e-6 - ang;
            const rho = spd * _this._pointerDy * _this._orientateInertia;
            const rh = - Math.max(Math.min(-rho, rax), rin);

            const rotH = Quaternion.RotationAxisToRef(left, rh, _this._tq0);
            const rotV = Quaternion.RotationAxisToRef(Axis.Y, spd * _this._pointerDx * _this._orientateInertia, _this._tq1);
            tar.applyRotationQuaternionInPlace(rotH);
            tar.applyRotationQuaternionInPlace(rotV);
            tar.addInPlace(cam.globalPosition);
            cam.setTarget(tar);
            cam.update();
            if (_this._orientateInertia <= 0.01) {
                return true;
            }
        }, 0, 200, this._orientateReq);
    }

    private static setFightInput(stage: DefaultStage): void {
        const _this = InputConfig;

        const dropKeys = [DeviceInputKey.keyboardSpace];
        DeviceInputPreset.onInput((e) => {
            UAV.dropBomb();
        }, InputStatus.click, ...dropKeys);
    }

    private static setGeneralCameraInput(stage: DefaultStage): void {
        const _this = InputConfig;
        let my = 0;
        let mx = 0;

        //..................................................................forward
        const forwardKeys = [DeviceInputKey.keyboardW];
        DeviceInputPreset.onInput((e) => {
            _this._keyState.add(KEY.forward);
            my = 1;
            _this.movementAction(stage, true, mx, my);
        }, InputStatus.down, ...forwardKeys);
        DeviceInputPreset.onInput((e) => {
            _this._keyState.remove(KEY.forward);
            my = 0;
            _this.movementAction(stage, false, mx, my);
        }, InputStatus.up, ...forwardKeys);
        //..................................................................backward
        const backwardKeys = [DeviceInputKey.keyboardS];
        DeviceInputPreset.onInput((e) => {
            _this._keyState.add(KEY.backward);
            my = -1;
            _this.movementAction(stage, true, mx, my);
        }, InputStatus.down, ...backwardKeys);
        DeviceInputPreset.onInput((e) => {
            _this._keyState.remove(KEY.backward);
            my = 0;
            _this.movementAction(stage, false, mx, my);
        }, InputStatus.up, ...backwardKeys);
        //..................................................................left
        const leftKeys = [DeviceInputKey.keyboardA];
        DeviceInputPreset.onInput((e) => {
            _this._keyState.add(KEY.left);
            mx = -1;
            _this.movementAction(stage, true, mx, my);
        }, InputStatus.down, ...leftKeys);
        DeviceInputPreset.onInput((e) => {
            _this._keyState.remove(KEY.left);
            mx = 0;
            _this.movementAction(stage, false, mx, my);
        }, InputStatus.up, ...leftKeys);
        //..................................................................right
        const rightKeys = [DeviceInputKey.keyboardD];
        DeviceInputPreset.onInput((e) => {
            _this._keyState.add(KEY.right);
            mx = 1;
            _this.movementAction(stage, true, mx, my);
        }, InputStatus.down, ...rightKeys);
        DeviceInputPreset.onInput((e) => {
            _this._keyState.remove(KEY.right);
            mx = 0;
            _this.movementAction(stage, false, mx, my);
        }, InputStatus.up, ...rightKeys);
    }

    private static movementAction(stage: DefaultStage, play: boolean, dx: number, dy: number): void {
        const computeDirection = () => {
            const forward = this.getCamDirection(stage, this._tv0).normalize();
            const left = Vector3.CrossToRef(forward, Axis.Y, this._tv1).normalize();
            forward.scaleInPlace(dy);
            left.scaleInPlace(dx);
            if (dx || dy) this.moveDirection.copyFrom(forward).addInPlace(left).normalize();
        };
        if (!play) {
            computeDirection();
            if (this._keyState.notExist(KEY.forward, KEY.backward, KEY.left, KEY.right)) this._attenuation = 0.98;
        } else {
            computeDirection();
            this._inertia = Math.min(Math.sqrt(dx! * dx! + dy! * dy!), 1);
            this._attenuation = 1;
        }
        if (AnimationFrame.isRunning(this._movementReq)) return;
        const dir = this._tv2;
        this._movementReq.observable = stage.mainScene.onBeforeRenderObservable;
        const _this = InputConfig;
        AnimationFrame.request((t: number) => {
            const cam = stage.mainCamera as UniversalCamera;
            const engine = stage.engine;
            const spd = _this.moveSpeed * Math.sqrt(t / (engine.getFps() * 100));
            dir.copyFrom(_this.moveDirection);
            _this._inertia *= _this._attenuation;
            dir.scaleInPlace(spd * _this._inertia);
            cam.position.addInPlace(dir);
            cam.cameraDirection.addInPlace(dir);
            cam.update();
            if (_this._inertia <= 0.01) {
                return true;
            }
        }, 0, 200, this._movementReq);
    }

    private static getCamDirection(stage: DefaultStage, out: Vector3): Vector3 {
        Helper.cameraDirection(stage.mainCamera, out);
        out.y = 0;
        out.normalize();
        return out;
    }

}


export { InputConfig };